package com.ke.rule.base;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.MDC;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 代码片段日志跟踪
 * 
 * @author guyu
 *
 */
public class SnippetsTrace {

  private final static String MDC_SECTION_TRACE = "snippetsTraceId";

  /**
   * 起点
   */
  private static class Zero {
    private String id;      // 段调用Id, 用于标识同一个方法内同一次调用
    private long start;     // 开始计时
    private boolean debugLevel; // 输出debug级别的日志
  }

  /**
   * 同一个线程内可能会有多个方法嵌套调用，使用stack管理
   */
  private static final ThreadLocal<Stack<Zero>> traces = new ThreadLocal<>();

  /**
   * 开始并返回调用Id
   * 
   * @return traceId
   */
  public static String begin() {
    return begin(true);
  }
  
  /**
   * 开始并返回调用Id
   * @param debugLevel
   * 
   * @return traceId
   */
  public static String begin(boolean debugLevel) {
    Stack<Zero> stack = traces.get();
    if (null == stack) {
      stack = new Stack<Zero>();
      traces.set(stack);
    }

    Zero zero = new Zero();
    zero.debugLevel = debugLevel;
    zero.start = System.currentTimeMillis();
    zero.id = Long.toHexString(System.currentTimeMillis())
        + RandomStringUtils.randomAlphanumeric(5).toLowerCase();

    // 压栈
    stack.push(zero);

    MDC.put(MDC_SECTION_TRACE, zero.id);
    return zero.id;
  }

  /**
   * 结束并返回耗时
   * 
   * @param traceId begin方法的返回值
   * @return cost 耗时毫秒
   */
  
  public static long end(String traceId) {
    return System.currentTimeMillis() - _end(traceId).start;
  }
  
  /**
   * 结束调用栈
   * 
   * @param traceId
   * @return
   */
  private static Zero _end(String traceId) {
    Stack<Zero> stack = traces.get();

    // 弹栈，有些情况下begin和end之间可能有异常,导致彼此不配对,需要持续Pop
    Zero zero = null;
    do {
      zero = stack.pop(); // if stack.isEmpty() will throw EmptyStackException
    } while (!zero.id.equals(traceId) && !stack.isEmpty());

    String presentTraceId;
    if (stack.isEmpty()) {
      traces.remove();
      presentTraceId = "";
    } else {
      // 返回上一层
      presentTraceId = stack.peek().id;
    }
    // 切换MDC
    MDC.put(MDC_SECTION_TRACE, presentTraceId);

    return zero;
  }

  /**
   * 实参
   */
  public static class Argument {
    private String name;
    private Object value;

    private Argument(String name, Object value) {
      super();
      this.name = name;
      this.value = value;
    }

    /**
     * 将参数值格式化为字符串，同时为了避免写入日志量较大做截断操作,目前设定上限为4K
     * 
     * @param value
     * @return
     */
    public static String formatAndTruncate(Object value) {
      if (null == value) {
        return null;
      }
      String str = null;

      if (value.getClass().isArray()) {
        Object[] a = (Object[]) value;
        if (a.length <= 3) {
          str = Arrays.deepToString(a);
        } else {
          Object[] subA = ArrayUtils.subarray(a, 0, 3);
          str = "length=" + a.length + " elements=" + Arrays.deepToString(subA);
        }
      } else if (value instanceof Collection) {
        Collection<?> c = (Collection<?>) value;
        if (c.size() <= 3) {
          str = String.format("[%s]", c.stream ().map (i -> i.toString ()).collect (Collectors.joining (",")));
        } else {
          Object[] subA = new Object[] { null, null, null };
          
          int i = 3;
          for (Iterator<?> it = c.iterator(); it.hasNext() && --i >= 0;) {
            subA[i] = it.next();
          }
          str = "size=" + c.size() + " elements=" + Arrays.deepToString(subA);
        }
      } else if (value instanceof Date) {
        str = DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT.format((Date) value);
      } else if (value instanceof Calendar) {
        str = DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT.format((Calendar) value);
      } else {
        str = value.toString();
      }

      StringBuilder builder = new StringBuilder(StringUtils.left(str, 4096));
      if (str.length() > 4096) {
        builder.append(" ...");
      }

      return builder.toString();
    }

    /**
     * to log string
     * 
     * @return
     */
    public String toLogString() {
      if (null == value) {
        return StringUtils.EMPTY;
      }
      
      StringBuilder builder = new StringBuilder();

      if (StringUtils.isNotBlank(name)) {
        builder.append(name).append(": ");
      }
      builder.append(formatAndTruncate(value));

      return builder.toString();
    }
  }

  /**
   * I/O参数构造
   * 
   * @param name
   * @param value
   * @return
   */
  public static Argument[] cast(String name, Object value) {
    return new Argument[] {new Argument(name, value)};
  }

  /**
   * I/O参数构造
   * 
   * @param pairs
   * @return
   */
  @SafeVarargs
  public static Argument[] cast(Pair<String, ? extends Object>... pairs) {
    if (null == pairs) {
      return null;
    }
    Argument[] args = new Argument[pairs.length];
    for (int i = 0; i < args.length; i++) {
      args[i] = new Argument(pairs[i].getKey(), pairs[i].getValue());
    }
    return args;
  }
  
  /**
   * 结束跟踪并写入日志
   * 
   * @param traceId
   * @param logger
   * @param method
   * @param inArgs
   */
  public static void endThenLog(String traceId, Logger logger, String method, Argument... inArgs) {
    endThenLog(traceId, logger, method, inArgs, null);
  }
  
  /**
   * 结束跟踪并写入日志
   * 
   * @param traceId
   * @param logger
   * @param method
   * @param inArgs
   * @param th
   * @param outArgs
   */
  public static void endThenLog(String traceId, Logger logger, String method, Throwable th,
      Argument... outArgs) {
    endThenLog(traceId, logger, method, new Argument[0], th, outArgs);
  }

  /**
   * 结束跟踪并写入日志
   * 
   * @param traceId begin方法的返回值
   * @param logger 日志
   * @param method 方法名称
   * @param inArgs 入参
   * @param throwable 异常
   * @param outArgs 出参/返回值
   */
  public static void endThenLog(String traceId, Logger logger, String method, Argument[] inArgs,
      Throwable throwable, Argument... outArgs) {
    StringBuilder inParaBuilder = new StringBuilder();

    if (null != inArgs && inArgs.length > 0) {
      for (Argument arg : inArgs) {
        if (inParaBuilder.length() > 0) {
          inParaBuilder.append(", ");
        }
        inParaBuilder.append(arg.toLogString());
      }
    }

    StringBuilder builder = new StringBuilder();
    builder.append(method).append("(").append(inParaBuilder).append(") ");

    if (null == throwable) {
      StringBuilder outParaBuilder = new StringBuilder();
      if (null != outArgs && outArgs.length > 0) {
        for (Argument arg : outArgs) {
          if (outParaBuilder.length() > 0) {
            outParaBuilder.append(", ");
          }
          outParaBuilder.append(arg.toLogString());
        }
      }
      builder.append("return");
      if (outParaBuilder.length() > 0) {
        builder.append(" (").append(outParaBuilder).append(")");
      }
    } else {
      builder.append("threw (error: ").append(ExceptionUtils.getRootCauseMessage(throwable)).append(")");
    }

    Zero zero = _end(traceId);
    builder.append(", cost: ").append(System.currentTimeMillis() - zero.start).append(" ms");

    if (null != throwable && !(throwable instanceof BusinessException)) {
      // 非业务异常输出error日志，业务异常会抛到前端展示
      logger.error(builder.toString());
    } else {
      if (logger.isInfoEnabled() && !zero.debugLevel) {
        logger.info(builder.toString());
      } else if (logger.isDebugEnabled()) {
        logger.debug(builder.toString());
      }
    }
  }
}
